#ifndef HMS_PROBE_HPP
#define HMS_PROBE_HPP

#include "solverBase.hpp"
#include "streamAccess.hpp"
#include "streamEigen.hpp"
#include "topology.hpp"
#include "settingsImport.hpp"

#include <filesystem>
#include <experimental/memory>

namespace hms
{
using std::experimental::observer_ptr;
using std::experimental::make_observer;
namespace fs = std::filesystem;

enum class ProbeType {
	none,
	cell,
	line,
};

template<typename _SolverType>
class Probe
{
public:
	using SolverType = _SolverType;
	ProbeSettings settings;

private:
	observer_ptr<const SolverType> m_solver  {nullptr};
	observer_ptr<const fs::path  > m_caseDir {nullptr};
	fs::path m_filePath;
	IdxVector m_cells;

public:
	Probe() = default;

	Probe(
		ProbeSettings&&,
		const fs::path& caseDir,
		const SolverBase<SolverType>& solver
	);

	void caseDir( const fs::path& );
	auto caseDir() const -> const fs::path&;

	void filePath( const fs::path&, const std::string& );
	auto filePath() const -> const fs::path&;

	void solver( const SolverBase<SolverType>& );
	auto solver() const -> const SolverType&;

	void cells( IdxVector&& );
	void cells( Array2Xs&& );
	auto cells() const -> const IdxVector&;

	void write() const;
};

#define CHEAD(RET) \
template<typename SolverType> RET Probe<SolverType>::

CHEAD()
Probe(
	ProbeSettings&& settingsArg,
	const fs::path& caseDirArg,
	const SolverBase<SolverType>& solverArg
) :
	settings { std::move(settingsArg) }
{
	solver(solverArg);
	caseDir(caseDirArg);
	filePath( caseDir(), settings.fileName );
	cells( std::move(settings.coords) );
}

CHEAD(auto) caseDir() const -> const fs::path& {
	assert( m_caseDir );
	return *m_caseDir;
}
CHEAD(void) caseDir( const fs::path& arg ){
	m_caseDir = make_observer(&arg);
}

CHEAD(auto) solver() const -> const SolverType& {
	assert( m_solver );
	return *m_solver;
}
CHEAD(void) solver(const SolverBase<SolverType>& arg){
	m_solver = make_observer( &( arg.derived() ) );
}

CHEAD(void) filePath( const fs::path& dir, const std::string& filename ){
	m_filePath = dir / filename;
}
CHEAD(auto) filePath() const -> const fs::path& { return m_filePath; }

CHEAD(auto) cells() const -> const IdxVector& { return m_cells; }
CHEAD(void) cells( IdxVector&& arg ){
	m_cells = std::move(arg);
}
CHEAD(void) cells( Array2Xs&& coords ){
	log::logger& logger { solver().logger() };
	if ( coords.cols() == 0 ){
		logger.warn("No coordinates passed to probe. Disabling...");
		settings.enabled = false;
		return;
	}
	using MeshType = typename SolverType::MeshType;
	const MeshType& mesh { solver().mesh() };

	Array2s beg {coords.col(0)}, end;
	if ( coords.cols() == 1 ){
		std::optional<Index> cell { containingCell(mesh, beg) };
		if ( !cell.has_value() )
			throw std::runtime_error( fmt::format(
				"No cell found at coordinates [{}]!", beg.transpose()
			));
		m_cells.push_back( cell.value() );
		return;
	}

	for ( Index i{1}; i<coords.cols(); ++i ){
		end = coords.col(i);
		IdxVector cellLine { cellsAlongLine(mesh, beg, end) };
		m_cells.insert( m_cells.end(), cellLine.begin(), cellLine.end() );
		beg.swap(end);
	}
	logger.trace(
		"Cells used by probe:\n{}",
		Eigen::Map<Array1Xi>{
			m_cells.data(),
			static_cast<Index>( m_cells.size() )
		}
	);
}

CHEAD(void) write() const {
	if (!settings.enabled)
		return;

	using S = SolverType;
	using FieldName = typename S::FieldName;
	using magic_enum::enum_name;

	const Time& time { solver().time() };
	using MeshType = typename SolverType::MeshType;
	const MeshType& mesh { solver().mesh() };

	std::string ee {"--"}, sep {settings.colSep};

	std::stringstream buf;

	auto forAllFields = [&]( auto&& func ){
		for ( int i{0}; i < S::nFields ; ++i ){
			FieldName fieldName { static_cast<FieldName>(i) };
			func(i, fieldName);
		}
	};

	if ( time.atStart() ){
		/* first line is x-coords,
		 * second is y-coords,
		 * then field names,
		 * then actual entries */
		std::stringstream x, y, fieldBuf;
		forAllFields( [&](int, FieldName fn){
			fieldBuf << sep << enum_name(fn);
			for ( Index cellIndex : cells() ){
				Vector2s centroid { mesh.cell(cellIndex).centroid() };
				for ( Index i{0}; i<solver().field(fn).nComponents(); ++i ){
					x << sep << centroid.x();
					y << sep << centroid.y();
					if (cellIndex != cells()[0] || i!=0)
						fieldBuf << sep << ee;
				}
			}
		});
		buf
			<< "x" << x.str() << '\n'
			<< "y" << y.str() << '\n'
			<< "Time[s]" << fieldBuf.str() << '\n';
	}

	buf << time.current();


	Eigen::IOFormat iof{
		hms::io::writePrecision, /* write precision (default: StreamPrecision) */
		Eigen::DontAlignCols,    /* flags (default: 0) */
		sep               /* coeffSeparator (default: " ") */
	};

	/* now the actual cell values are written */
	forAllFields( [&](int, FieldName fn){
		for ( Index cellIndex : cells() ){
			buf << sep <<
				solver().field(fn).values().col(cellIndex)
				.transpose()
				.format(iof);
		}
	});

	std::ofstream of;
	of.open( filePath().c_str(),
		time.atStart() ? std::ios_base::trunc : std::ios_base::app
	);
	if (!of)
		throw FileError(fmt::format(
			"Can't write to file \"{}\"!",
			bold("{}", filePath().string() )
		));

	of << buf.str() << '\n';
	of.close();
}

#undef CHEAD

} // namespace hms

#endif